Skip to content

feat(options): a reachable collect heartbeat, and name the pointer-width route boundary - #32

Merged
Ttimmahlax merged 3 commits into
mainfrom
small-path-step
Sep 10, 2026
Merged

feat(options): a reachable collect heartbeat, and name the pointer-width route boundary#32
Ttimmahlax merged 3 commits into
mainfrom
small-path-step

Conversation

@Ttimmahlax

Copy link
Copy Markdown
Contributor

Closes docs/plans/fixed-prim-small-step.md (now in finished/, §8), filed by the Kairos RTOS.

The correction is the finding

They measured a 16% step at 512 bytes on a 32-bit ESP32-S3, could not reproduce it on a 64-bit host, and concluded it points at prim::fixed. It does not.

SMALL_SIZE_MAX is 128 * size_of::<usize>()1,024 on their host, 512 on the device, where it coincides with SMALL_OBJ_SIZE_MAX. Their refutation of that constant was run on a machine where the suspect is not at the scene. Moving only the pointer width, prim::windows in both arms:

arm 256 vs 264 (control) 512 vs 513 2048 vs 2049
x86-64 host −0.9% +0.1% −81.9%
i686 host −0.4% +8.6% −80.6%

ABBA-interleaved, 50,000 ops/arm, min of 40 blocks. The control resolves under 1%.

Mechanism, counted not timed

Per 100,000 pairs, one block live:

route generic/op pages carved extends retires
direct[] (<= SMALL_SIZE_MAX) 1.0000 195 195 195
bin peek, just above 1.0000 0 0 0

Both take the slow path every op, so that is not it. 100_000 / 512 = 195 is the GENERIC_COLLECT sweep. The boundary is the route, not the page kind: on 64-bit, 513–1024 are medium pages that still churn.

What ships

  • --cfg ra_generic_collect="64" | "4096" | "65536". The period is a genuine trade and bare metal could not move it: options::set is a no-op under ONE_REGION, and the option's own doc told firmwares to change a built-in default that had no cfg. Default unchanged — 10,000 starved this profile once already (168 → 8 blocks, 22,533/50,000 nulls).
  • prim::fixed::shape_of(size) -> Shape — page bytes, dedicated segments, direct_route; const, geometry-derived. The observability §5 asked for, which region_stats() cannot give because it reports over extents.
  • A unit test pinning SMALL_SIZE_MAX == 128 * size_of::<usize>() and both width-specific values.

Two of their §5 gaps were already reachable: alloc::usable_size and alloc::stats() (which is the slow-path counter they wanted). Both live in alloc, not prim::fixed, so a seam re-exporting the fixed-region API in one pub use cannot see them — same shape as the PrimError gap closed in 2.1.0.

Not established

How much of the 16% the churn accounts for. Raising the heartbeat took churn to a measured zero, but that run's timing control flipped +8.0% → +0.6%, so it is withdrawn rather than quoted. The device is the right box; --cfg ra_generic_collect="65536" against their own sweep is the one-line experiment, and stats().pages_fresh needs no quiet core.

Gates

20 suites at both profiles, gate-selftest 11/11, wasm 20,168 (baseline 20,169), clippy 0. rusty_alloc builds clean for i686-pc-windows-msvc; only generator-0.8.9 (a loom dev-dep) blocks running the harness there.

🤖 Generated with Claude Code

tim-almond-house and others added 3 commits September 10, 2026 15:28
…dth route boundary

The Kairos RTOS reported one alloc+free stepping 16% across 512 bytes on a
32-bit ESP32-S3, absent on a 64-bit host, and concluded it points at
`prim::fixed`. It does not.

`SMALL_SIZE_MAX` is `128 * size_of::<usize>()`: 1,024 on their host, 512 on the
device, where it coincides with `SMALL_OBJ_SIZE_MAX`. Their refutation of that
constant was run on a machine where it sits at 1,024. Moving only the pointer
width, with `prim::windows` in both arms (ABBA, 50k ops/arm, min of 40 blocks):
x86-64 +0.1% at 512 vs 513, i686 +8.6%, control under 1% on both.

Mechanism, counted rather than timed: per 100,000 pairs with one block live the
`direct[]` route carves/extends/retires 195 pages and the bin route 0, while
`generic`/op is 1.0000 on both -- so the slow path is not the difference. 195 is
100,000/512, the GENERIC_COLLECT sweep at the small profile.

That period is a real trade (churn against capacity decay) and bare metal could
not move it: `options::set` is a no-op under ONE_REGION and the option's doc
told firmwares to change a built-in default that had no cfg. Adds
`--cfg ra_generic_collect`; the default does not move, because 10,000 starved
this profile once already.

Also adds `prim::fixed::shape_of(size) -> Shape` (page bytes, dedicated
segments, direct_route) so the route and page kind are a const answer instead of
a silicon discovery -- the observability the report asked for, which
`region_stats()` cannot give because it reports over extents.

Not established, and withdrawn rather than quoted: how much of the 16% the churn
accounts for. Raising the heartbeat took churn to a measured zero, but that
run's timing control flipped +8.0% -> +0.6%, so the instrument was deciding it.

Plan closed: docs/plans/finished/fixed-prim-small-step.md section 8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The new pointer-width test carried `assert_ne!(SMALL_SIZE_MAX,
SMALL_OBJ_SIZE_MAX)` on its 64-bit branch. That is a fact about the GEOMETRY,
not the pointer, and it is false under `ra_segment_size="256k"`: an 8 KiB slice
puts SMALL_OBJ_SIZE_MAX at 1024, which is where the 64-bit route top already
is. Green at the default slice, red on the 256k rung -- the exact defect shape
this repo's own notes describe, committed in the commit that documented it.

Derived from the arithmetic now, so it says something at every geometry. The
coincidence is also the interesting half: where the two constants land on the
same byte, one step hides two boundaries and a sweep cannot separate them,
which is what made the reporting device confusing in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tracing collect_inner's reclaim gives the churn a face and one unexpected
column: every reclaimed page carries capacity=1. `page_extend` links a 4 KiB
payload bound, so a 512-byte class should return 8 blocks and a 1024-byte class
4. A page holding one block is exhausted by the next allocation, which is the
missing half of why `generic` reads exactly 1.0000 per op.

The trace also pins the route boundary exactly: bins 20/21/24 (the direct[]
sizes at SMALL_SIZE_MAX=1024) churn ~200 times per 100k pairs, bins 25 and 28
just above it churn once.

Recorded, not chased: it points at page_extend/page_fresh, which is a hot-path
change needing the instruction-count battery rather than a session's tail. It
is a better lead than the heartbeat knob, because it would close the step from
the other side with no default moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants